/**
 * Bilibili RESTful API
 *
 * 本实现希望通过调用尽量少的 RESTful 方法来完成更多功能，方便协议升级后进行改动
 */

import axios from 'axios';
import * as querystring from 'querystring';
import { Await, sleep } from './utils';


/** 原始直播间信息类型 */
interface _RawLiveRoomInfoT {
  /** 直播间标题 */
  title: string,
  /** 主播 UID */
  uid: number,
  /** 在线人数（不过我觉得大概是 _人气_ */
  online: number,
  /** 简介 */
  description: string,
  /** 直播状态（0 未开播；1 直播中；2 轮播中） */
  live_status: 0 | 1 | 2,
  /** 总直播分区 ID */
  parent_area_id: number,
  /** 子直播分区 ID */
  area_id: number,
  /** 总直播分区名称（e.g. “虚拟主播”） */
  parent_area_name: string,
  /** 关键词，CSV */
  tags: string
}

let liveroom_info_cache = new Map<number, _RawLiveRoomInfoT>();

/**
 * 获取直播间详细信息
 * @param roomid - 直播间 ID
 * @param [opt.force_cache_miss=false] - 强制缓存失效
 * @param [opt.retries=3] - 重试次数，若一次请求失败，则过一段时间后重发，直至次数耗尽
 * @returns {_RawLiveRoomInfoT} 具体格式见补全，懒得写了 @see {@link _RawLiveRoomInfoT}
 */
export async function _get_liveroom_info_raw(roomid: number,
      opt?: {force_cache_miss?: boolean, retries?: number})
      : Promise<_RawLiveRoomInfoT> {
  if (typeof opt === 'undefined')
    opt = {};
  opt.force_cache_miss = opt.force_cache_miss ?? false;
  opt.retries = opt.retries ?? 3;
  if (opt.force_cache_miss)
    liveroom_info_cache.delete(roomid);
  // read from cache first
  if (liveroom_info_cache.has(roomid))
    return liveroom_info_cache.get(roomid)!;
  // read from remote
  let resp = await axios.get(
    'http://api.live.bilibili.com/room/v1/Room/get_info',
    {params: {id: roomid}, validateStatus: stt => true}
  );
  if (resp.status !== 200) {
    if (opt.retries < 0) {
      console.debug(resp);
      throw new Error('Bilibili API request failed');
    }
    console.warn('Bilibili API request failed', resp, 'wait and retry');
    await sleep(8000 / Math.pow(2, opt.retries));
    return await _get_liveroom_info_raw(roomid,
        {force_cache_miss: true, retries: opt.retries - 1});
  }
  let data: {code: number, data: _RawLiveRoomInfoT} = resp.data;
  if (data.code !== 0)
    throw new Error('Bilibili API request error');
  // write to cache
  liveroom_info_cache.set(roomid, data.data);
  setTimeout(() => {liveroom_info_cache.delete(roomid)}, 5000);
  return data.data;
}

/**
 * 由直播间号获取主播 UID
 * @param room_id 直播间号
 * @returns UID
 */
export async function get_uid_of_room(room_id: number): Promise<number> {
  return (await _get_liveroom_info_raw(room_id)).uid;
}

/** 原始用户信息类型 */
interface _RawUserInfoT {
  /** 用户信息 */
  info: {
    uid: number,
    /** 用户名 */
    uname: string,
  },
  /** 直播间号，若为 0 则未开通直播间 */
  room_id: number,
}

let user_info_cache = new Map<number, _RawUserInfoT>();

/**
 * 获取用户详细信息（空间视图）
 * @param uid
 * @param SESSDATA
 * @param [opt.force_cache_miss=false] - 强制缓存失效
 * @param [opt.retries=3] - 重试次数，若一次请求失败，则过一段时间后重发请求，直至次数耗尽
 * @returns {_RawUserInfoT} 具体格式见补全，懒得写了 @see {@link _RawUserInfoT}
 */
export async function _get_user_info_space_raw(uid: number, SESSDATA?: string,
      opt?: {force_cache_miss?: boolean, retries?: number}) : Promise<_RawUserInfoT> {
  if (typeof opt === 'undefined')
    opt = {};
  opt.force_cache_miss = opt.force_cache_miss ?? false;
  opt.retries = opt.retries ?? 3;
  if (opt.force_cache_miss)
    user_info_cache.delete(uid);
  // read from cache
  if (user_info_cache.has(uid))
    return user_info_cache.get(uid)!;
  // read from remote
  let resp = await axios.get(
    'http://api.live.bilibili.com/live_user/v1/Master/info',
    {
      params: {uid: uid},
      headers: typeof SESSDATA === 'undefined' ? undefined : {Cookie: `SESSDATA=${SESSDATA}`},
      validateStatus: stt => true
    }
  );
  if (resp.status !== 200) {
    if (opt.retries < 0) {
      console.debug(resp);
      throw new Error(`Bilibili API request failed`);
    }
    console.warn('Bilibili API request failed', resp, 'wait and retry');
    await sleep(8000 / Math.pow(2, opt.retries));
    return await _get_user_info_space_raw(uid, SESSDATA,
        {force_cache_miss: true, retries: opt.retries - 1});
  }
  let data: {code: number, data: _RawUserInfoT} = resp.data;
  if (data.code !== 0)
    throw new Error('Bilibili API request error');
  // write to cache
  user_info_cache.set(uid, data.data);
  setTimeout(() => {user_info_cache.delete(uid);}, 5000);
  return data.data;
}

/**
 * 由主播 UID 获取直播间号
 * @param uid 主播 UID
 * @returns room_id 直播间号，若未开通直播间则返回 undefined
 */
export async function get_room_of_uid(uid: number): Promise<number | undefined> {
  let data = await _get_user_info_space_raw(uid);
  return data.room_id === 0 ? undefined : data.room_id;
}

/**
 * 由主播 UID 获取主播名称
 * @param uid 主播 UID
 * @returns uname 主播昵称
 */
export async function get_uname_of_uid(uid: number): Promise<string> {
  return (await _get_user_info_space_raw(uid)).info.uname;
}

/**
 * 查询是否将该用户设为特别关注
 * @param fid - 目标用户 UID
 * @param SESSDATA - 本用户会话
 * @returns 是否为特别关注，若还未关注该用户则返回 undefined
 */
export async function is_special_follow(fid: number, SESSDATA: string)
      : Promise<boolean | undefined> {
  let resp = await axios.get(
    'http://api.bilibili.com/x/relation',
    {
      params: {fid: fid},
      headers: {Cookie: `SESSDATA=${SESSDATA}`}
    }
  );
  let data: {code: number, data: {attribute: number, special: number}} = resp.data;
  if (data.code !== 0)
    throw new Error('Bilibili API request error');
  if (data.data.attribute === 0)
    return undefined;
  return data.data.special === 1;
}

/**
 * 判断主播是否为虚拟主播（通过查询主播当前直播间分区设置实现）
 * @param roomid
 * @returns 是否是虚拟主播
 */
export async function is_vup_room(roomid: number): Promise<boolean> {
  return (await _get_liveroom_info_raw(roomid)).parent_area_name === '虚拟主播';
}

/**
 * 获取用户信息
 * @param uid
 * @param [SESSDATA]
 * @returns （见补全
 */
export async function get_user_info(uid: number, SESSDATA?: string)
      : Promise<{
        /** 用户昵称 */
        name: string,
        /** 是否特别关注，若未提供 SESSDATA 或为关注则返回 undefined */
        special?: boolean,
        /** 直播间号，若未开通直播间则返回 undefined */
        roomid?: number,
        /** 是否为虚拟主播，若未开通直播间则返回 undefined */
        is_vup?: boolean
      }> {
  let name = await get_uname_of_uid(uid);
  let special: boolean | undefined = undefined;
  if (typeof SESSDATA !== 'undefined') {
    special = await is_special_follow(uid, SESSDATA);
  }
  let roomid = await get_room_of_uid(uid);
  let is_vup_: boolean | undefined = undefined;
  if (typeof roomid !== 'undefined')
    is_vup_ = await is_vup_room(roomid);
  return {
    name: name,
    special: special,
    roomid: roomid,
    is_vup: is_vup_
  };
}

/**
 * 检查是否正开播？
 * @param [uid] - 主播 UID
 * @param [room_id] - 直播间 ID
 */
export async function check_door_open(room_id: number): Promise<boolean> {
  return (await _get_liveroom_info_raw(room_id, {force_cache_miss: true})).live_status === 1;
}

interface _RawFollowItemT {
  /** 用户 UID */
  uid: number,
  /** 用户昵称 */
  uname: string,
  /** 用户签名 */
  sign: string,
  /** 是否特别关注，若为提供 SESSDATA 则为 undefined，但需要注意的是，
   * 若提供了无效的 SESSDATA 则仍然会返回 false，该歧义接口限制导致的
   */
  special?: boolean
}

/**
 * 获取用户关注列表分页
 * @param uid - 用户 UID
 * @param pn - 页号 [1,inf
 * @param [SESSDATA] - 验证 Cookie
 * @returns 本页关注用户信息列表
 * @throws {Error('no auth')} 提供的 `SESSDATA` 验证不匹配
 */
export async function _get_follow_page(
      uid: number, pn: number, SESSDATA?: string)
      : Promise<{
          /** 本页关注 */
          list: _RawFollowItemT[],
          /** 关注总数 */
          total: number
        }> {
  let resp = await axios.get(
    'http://api.bilibili.com/x/relation/followings',
    {
      params: { 'vmid': uid, 'pn': pn },
      headers: typeof SESSDATA === 'undefined' ? undefined : { 'Cookie': `SESSDATA=${SESSDATA}` }
    }
  );
  let data: {
      code: number,
      data?: { list: { mid: number, special: 0 | 1, uname: string,
                       sign: string }[],
               total: number }
  } = resp.data;
  if (data.code === 22007)
    throw new Error('no auth');
  if (data.code !== 0)
    throw new Error('unknown');
  let page = [];
  for (let { mid, uname, special, sign } of data.data!.list)
    page.push({uid: mid, uname: uname, sign: sign,
        special: typeof SESSDATA === 'undefined' ? undefined : special === 1});
  return {list: page, total: data.data!.total};
}

/**
 * 获取用户关注列表
 * @param uid - 用户 UID
 * @param SESSDATA - Cookie 验证信息
 * @returns 关注用户信息列表
 */
export async function get_follow_list(uid: number, SESSDATA: string)
      : Promise<Required<_RawFollowItemT>[]> {
  let follows = [];
  for (let pn = 0; ; pn++) {
    let fs = (await _get_follow_page(uid, pn, SESSDATA)).list;
    if (fs.length === 0)
      break;
    for (let f of fs) {
      follows.push({
        uid: f.uid, uname: f.uname, sign: f.sign, special: f.special!
      });
    }
  }
  return follows;
}

/**
 * 获取登录二维码
 * @return URL，以及一个用于识别当前申请的登录会话的 oauthKey
 */
export async function get_login_qrcode_url(): Promise<{url: string, oauthKey: string}> {
  let resp = await axios.get('http://passport.bilibili.com/qrcode/getLoginUrl');
  let data: {code: number, data: {url: string, oauthKey: string}} = resp.data;
  if (data.code !== 0)
    throw new Error('Bilibili API request error');
  return {url: data.data.url, oauthKey: data.data.oauthKey};
}

/**
 * 获取登录二维码扫描信息
 * @param oauthKey - 扫码登录密钥
 * @returns 登录是否成功
 * @throws {Error('timeout')} 二维码超时
 * @throws {Error(xxx)} 其他未分类处理错误错误
 */
export async function get_login_qrcode_info(oauthKey: string)
    : Promise<{uid: number, SESSDATA: string, expire_date: Date} | null | undefined> {
  let resp = await axios.post(
    'http://passport.bilibili.com/qrcode/getLoginInfo',
    querystring.stringify({oauthKey: oauthKey})
  );
  let data: {code: number, status: boolean, data: object | -1 | -2 | -4 | -5} = resp.data;
  let header: {DedeUserID: string, SESSDATA: string, Expires: string}
  /* 登录成功，提取并返回用户信息 */
  if (data.status) {
    let ret: {uid?: number, SESSDATA?: string, expire_date?: Date} = {};
    for (let c of resp.headers['set-cookie'] as string[]) {
      let m = c.match(/^DedeUserID=(?<uid>\d+);/);
      if (m !== null) {
        ret.uid = Number(m.groups!['uid']);
        continue;
      }
      m = c.match(/^SESSDATA=(?<SESSDATA>\S+);.+Expires=(?<expire_date>.+?);/);
      if (m !== null) {
        ret.SESSDATA = m.groups!['SESSDATA'];
        ret.expire_date = new Date(m.groups!['expire_date']);
        continue;
      }
    }
    if (typeof ret.uid === 'undefined' || typeof ret.SESSDATA === 'undefined'
          || typeof ret.expire_date === 'undefined') {
      throw new Error('success but field not fully extracted');
    }
    return {uid: ret.uid, SESSDATA: ret.SESSDATA, expire_date: ret.expire_date};
  }
  /* 失败处理逻辑 */
  if (typeof data.data !== 'number')
    throw new Error('Bilibili API updated');
  switch (data.data) {
    /** 未扫描或扫描失败 */
    case -4: case -5: return null;
    /** 超时 */
    case -2: throw new Error('timeout');
    case -1: throw new Error('wrong oauthKey');
    default:
      console.warn(`Bilibili threw something I cannot understand .data = ${data.data}, ignored`);
      return undefined;
  }
}
